using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace libACO
{
    public struct FileHeader
    {
        public string Path;         // Putanja filea
        public string Name;         // Naziv filea bez putanje
        public long Length;        // Velicina filea u byteima
        public long Offset;        // Offset filea u arhivi
        public byte StartingFile;   // Broj dijela arhive u kom file pocinje
        public byte Flags;          // Flags
    }

    public class ACOarchive
    {
        private const byte Version = 1; // Verzija arhive
        private const long BaseHeader = 13;
        private const long ExtHeader = 9;
        private const long FileHeaderSize = 209;

        private FileHeader[] mFiles;    // Podaci o fileovima u arhivi
        private Int32 mFilesNum;          // Broj fileova u arhivi
        private string[] mArchiveName;  // Nazivi arhiva
        //private byte mNumArchives;       // Broj arhiva
        private byte[] mSignature;      // Potpis arhive (4 byte)
        //private Int32 mHeaderSize;      // Duzina zaglavlja u byteima

        private int DefaultBufferSize = 1048567;
        // Konstruktor - otvaranje postojece arhive
        // archiveName - puna putanja arhive koja se otvara
        public ACOarchive(string archiveName)
        {
            ReadArchive(archiveName);
        }

        // Konstruktor - kreiranje arhive ubacujuci fileove sa liste
        // archiveName - puna putanja arhive koja se kreira
        // fileName[] - putanje fileova koji se ubacuju u arhivu
        public ACOarchive(string archiveName, string[] fileName)
        {
            CreateArchive(archiveName, fileName, 0);
        }

        public ACOarchive(string archiveName, string[] fileName, string[] altNames)
        {
            CreateArchive2(archiveName, fileName, altNames, 0);
        }

        public ACOarchive(string archiveName, List<string> fileNames)
        {
            string[] tNames = new string[fileNames.Count];
            int i = 0;
            foreach (string name in fileNames)
                tNames[i] = fileNames[i++];
            CreateArchive(archiveName, tNames, 0);
        }

        public ACOarchive(string archiveName, List<string> fileNames, string path)
        {
            string[] tNames = new string[fileNames.Count];
            int i = 0;
            foreach (string name in fileNames)
                tNames[i] = path + @"\" + fileNames[i++];
            CreateArchive(archiveName, tNames, 0);
        }
        // Konstruktor - kreiranje razdijeljene arhive ubacujuci fileove sa liste
        // archiveName - puna putanja arhive koja se kreira
        // fileName[] - putanje fileova koji se ubacuju u arhivu
        // splitLength - maksimalna duzina dijela arhive u byteima
        public ACOarchive(string archiveName, string[] fileName, Int32 splitLength)
        {
            CreateArchive(archiveName, fileName, splitLength);
        }

        // Metod - kreiranje razdijeljene arhive ubacujuci fileove sa liste
        // archiveName - puna putanja arhive koja se kreira
        // fileName[] - putanje fileova koji se ubacuju u arhivu
        // splitLength - maksimalna duzina dijela arhive u byteima

        /*
         *  3 bytes - magic number ("ACO")
            1 byte - version
            1 byte - head/part (0/1)
            4 byte - archive signature

            (fixed header = 9 bytes)

            4 byte - number of files in archive
    
            (head archive = 4 bytes, so far = 13 bytes)

            *** FILE HEADER ***
    
            128 bytes - path
            64 bytes - file name
            8 bytes - offset
            8 bytes - file size
            1 byte - flags

            (file header = 209 bytes)
         */

        public bool ReadArchive(string archiveName)
        {
            BinaryReader arc;
            try // Pokusaj otvaranja arhive
            {
                arc = new BinaryReader(File.Open(archiveName, FileMode.Open));
            }
            catch (Exception) { return false; }

            byte[] magic = new byte[3];
            //magic = arc.ReadBytes(3);   // Magicni broj (ACO)
            try
            {
                magic[0] = arc.ReadByte();
                magic[1] = arc.ReadByte();
                magic[2] = arc.ReadByte();
            }
            catch (Exception) { return false; }

            if (magic[0] != 65 || magic[1] != 67 || magic[2] != 79)
            {
                arc.Close();
                return false;
            }

            if (arc.ReadByte() != Version)  // Provjera verzije
            {
                arc.Close();
                return false;
            }

            if (arc.ReadByte() != 0)  // Mora biti pocetni file
            {
                arc.Close();
                return false;
            }

            mSignature = arc.ReadBytes(4);
            mFilesNum = arc.ReadInt32();
            mFiles = new FileHeader[mFilesNum];

            for (int i = 0; i < mFilesNum; ++i)
            {
                char[] path = arc.ReadChars(128);
                //mFiles[i].Path = path.ToString();
                //mFiles[i].Path = new string(path);
                mFiles[i].Path = CharArray2String(path);
                char[] name = arc.ReadChars(64);
                //mFiles[i].Name = name.ToString();
                //mFiles[i].Name = new string(name);
                mFiles[i].Name = CharArray2String(name);

                mFiles[i].Offset = arc.ReadInt64();
                mFiles[i].Length = arc.ReadInt64();

                mFiles[i].Flags = arc.ReadByte();
            }


            mArchiveName = new string[1];
            mArchiveName[0] = archiveName;
//            mNumArchives = 1;

            arc.Close();

            return true;
        }

        public bool CreateArchive(string archiveName, string[] fileName, Int32 splitLength)
        {
            BinaryWriter arc = new BinaryWriter(File.Open(archiveName, FileMode.Create));

            mFilesNum = fileName.Length;
            mFiles = new FileHeader[mFilesNum];
            for (int i = 0; i < fileName.Length; ++i)
            {
                //FileHeader tfh = GetHeaderFor(fileName[i]);
                mFiles[i] = GetHeaderFor(fileName[i]);
            }

            WriteHeader(arc);

            for (int i = 0; i < mFilesNum; ++i)
            {
                CopyFileToStream(arc, fileName[i], DefaultBufferSize);
            }

            arc.Close();
            return true;
        }

        public bool CreateArchive2(string archiveName, string[] fileName, string[] altNames, Int32 splitLength)
        {
            BinaryWriter arc = new BinaryWriter(File.Open(archiveName, FileMode.Create));

            mFilesNum = fileName.Length;
            mFiles = new FileHeader[mFilesNum];
            for (int i = 0; i < fileName.Length; ++i)
            {
                //FileHeader tfh = GetHeaderFor(fileName[i]);
                mFiles[i] = GetHeaderFor(fileName[i]);
                mFiles[i].Name = altNames[i];
            }

            WriteHeader(arc);

            for (int i = 0; i < mFilesNum; ++i)
            {
                CopyFileToStream(arc, fileName[i], DefaultBufferSize);
            }

            arc.Close();
            return true;
        }

        public void WriteHeader(string fileName)
        {
            BinaryWriter arc = new BinaryWriter(File.Open(fileName, FileMode.Create));
            WriteHeader(arc);
            arc.Flush();
            arc.Close();
        }

        public void WriteHeader(BinaryWriter arc)
        {
            arc.Write(new char[] { 'A', 'C', 'O' });    // Magicni broj
            arc.Write(Version);                     // Verzija
            arc.Write((byte)0);                     // Glavni dio arhive

            GenerateSignature();
            arc.Write(mSignature);                  // Potpis arhive

            //mFilesNum = fileName.Length;
            arc.Write(mFilesNum);                   // Broj fileova

            //mFiles = new FileHeader[mFilesNum];
//            mNumArchives = 0;
            //mArchiveName = new string[1];
            //mArchiveName[0] = archiveName;

            long offset = BaseHeader + mFilesNum * FileHeaderSize; // Pocetni offset

            for (int i = 0; i < mFilesNum; ++i)
            {
                //FileInfo fi = new FileInfo(mFiles[i].Name);    // Pronalazenje file infoa
                arc.Write(new byte[128]);                   // Putanja (WIP)
                arc.Write(String2ByteArray(mFiles[i].Name, 64));   // Ime filea
                arc.Write(offset);                          // Offset
                mFiles[i].Offset = offset;
                //arc.Write(fi.Length);                       // Velicina filea
                arc.Write(mFiles[i].Length);
                arc.Write(new byte());                      // Flags (WIP)
                offset += mFiles[i].Length;                          // Pomijeranje offseta
            }
        }

        private void GenerateSignature()
        {
            Random rnd = new Random();
            mSignature = new byte[4];
            rnd.NextBytes(mSignature);
        }

        private byte[] String2ByteArray(string txt, int Lenght)
        {
            char[] ca = txt.ToCharArray();
            byte[] ba = new byte[Lenght];
            for (int i = 0; i < Lenght; ++i)
                ba[i] = (i < ca.Length) ? (byte)ca[i] : (byte)0;
            return ba;
        }

        private string CharArray2String(char[] carray)
        {
            string tstr = "";
            foreach (char c in carray)
                if (c != '\0') tstr += c.ToString(); else break;
            return tstr;
        }

        private void CopyFileToStream(BinaryWriter bw, string fileName, int bufferSize)
        {
            FileInfo fi = new FileInfo(fileName);
            long flen = fi.Length;
            int tours = (int)(flen / bufferSize);
            int remain = (int)(flen % bufferSize);

            byte[] buffer = new byte[bufferSize];
            byte[] buffer1 = new byte[remain];

            BinaryReader br = new BinaryReader(File.Open(fileName, FileMode.Open));

            for (int i = 0; i < tours; ++i)
            {
                buffer = br.ReadBytes(bufferSize);
                bw.Write(buffer);
            }

            if (remain > 0)
            {
                buffer1 = br.ReadBytes(remain);
                bw.Write(buffer1);
            }

            br.Close();
        }

        public string[] FileNames
        {
            get
            {
                string[] names = new string[mFilesNum];
                for (int i = 0; i < mFilesNum; ++i)
                    names[i] = mFiles[i].Name;
                return names;
            }
        }

        public int NumberOfFiles
        {
            get
            {
                return mFilesNum;
            }
        }

        private int GetFileID(string fileName)
        {
            for (int i = 0; i < mFilesNum; ++i)
            {
                if (mFiles[i].Name == fileName) return i;
            }
            return -1;
        }

        public long GetFileSize(string fileName)
        {
            for (int i = 0; i < mFilesNum; ++i)
            {
                if (mFiles[i].Name == fileName) return mFiles[i].Length;
            }
            return 0;
        }

        public bool FileExists(string fileName)
        {
            return (GetFileID(fileName) >= 0);
        }

        public bool ExtractFile(string fileName, string outFolder)
        {
            int id = GetFileID(fileName);
            if (id < 0) return false;

            return ExtractFile(fileName, outFolder, mFiles[id].Name);
        }

        public FileStream GetFileStream(string fileName)
        {
            int id = GetFileID(fileName);
            if (id < 0) return null;

            //FileStream fs = new FileStream(mArchiveName[0], FileMode.Open, FileAccess.Read, FileShare.Read);
            //fs.Seek(mFiles[id].Offset, SeekOrigin.Begin);
            //LimitedFileStream lfs = new LimitedFileStream(fs, mFiles[id].Offset, mFiles[id].Length);
            LimitedFileStream lfs = new LimitedFileStream(mArchiveName[0], mFiles[id].Offset, mFiles[id].Length);
            return lfs;
        }

        public bool ExtractFileAs(string fileName, string outName)
        {
            int id = GetFileID(fileName);
            if (id < 0) 
                return false;

            FileInfo fi = new FileInfo(outName);
            return ExtractFile(fileName, fi.Directory.FullName, fi.Name);
        }

        public bool ExtractFile(string fileName, string outFolder, string outName)
        {
            int id = GetFileID(fileName);
            if (id < 0) 
                return false;

            BinaryReader br;

            try
            {
                br = new BinaryReader(File.Open(mArchiveName[0], FileMode.Open, FileAccess.Read));
            }
            catch (Exception e) 
            {
                System.Diagnostics.Debug.WriteLine(e);
                return false; 
            }

            br.BaseStream.Seek(mFiles[id].Offset, SeekOrigin.Begin);

            CopyStreamToFile(br, outFolder + "\\" + outName, mFiles[id].Length, DefaultBufferSize);

            br.BaseStream.Flush();
            br.BaseStream.Close();
            br.Close();

            return true;
        }

        private void CopyStreamToFile(BinaryReader br, string fileName, long flen, int bufferSize)
        {
            try
            {
                int tours = (int)(flen / bufferSize);
                int remain = (int)(flen % bufferSize);

                byte[] buffer = new byte[bufferSize];
                byte[] buffer1 = new byte[remain];

                BinaryWriter bw = new BinaryWriter(File.Open(fileName, FileMode.Create));

                for (int i = 0; i < tours; ++i)
                {
                    buffer = br.ReadBytes(bufferSize);
                    bw.Write(buffer);
                }

                if (remain > 0)
                {
                    buffer1 = br.ReadBytes(remain);
                    bw.Write(buffer1);
                }

                bw.Flush();
                bw.Close();
            }
            catch (Exception)
            {
            }
        }

        private void CopyStreamToStream(BinaryReader br, BinaryWriter bw, long flen, int bufferSize)
        {
            int tours = (int)(flen / bufferSize);
            int remain = (int)(flen % bufferSize);

            byte[] buffer = new byte[bufferSize];
            byte[] buffer1 = new byte[remain];

            for (int i = 0; i < tours; ++i)
            {
                buffer = br.ReadBytes(bufferSize);
                bw.Write(buffer);
            }

            if (remain > 0)
            {
                buffer1 = br.ReadBytes(remain);
                bw.Write(buffer1);
            }
        }

        public void UnpackTo(string targetFolder)
        {
            for (int i = 0; i < mFilesNum; ++i)
                ExtractFile(mFiles[i].Name, targetFolder);
        }

        public void ShiftOffsets(long value)
        {
            for (int i = 0; i < mFilesNum; ++i)
                mFiles[i].Offset += value;
        }

        public void AppendFile(string fileName)
        {
            if (!File.Exists(fileName)) return;

            ShiftOffsets(FileHeaderSize);

            FileHeader[] tFiles = new FileHeader[mFilesNum + 1];
            tFiles[mFilesNum] = GetHeaderFor(fileName);

            for (int i = 0; i < mFilesNum; ++i)
                tFiles[i] = mFiles[i];

            mFiles = tFiles;

            if (mFilesNum > 0)
                mFiles[mFilesNum].Offset = mFiles[mFilesNum - 1].Offset + mFiles[mFilesNum - 1].Length;
            else
                mFiles[mFilesNum].Offset = BaseHeader;


            BinaryWriter bw = new BinaryWriter(File.Open(mArchiveName[0] + "_", FileMode.Create));
            mFilesNum++;
            WriteHeader(bw);
            mFilesNum--;

            BinaryReader br = new BinaryReader(File.Open(mArchiveName[0], FileMode.Open));
            //for (int i = 0; i < BaseHeader + FileHeaderSize * mFilesNum; ++i)
            //    br.ReadByte();
            br.BaseStream.Seek(BaseHeader + FileHeaderSize * mFilesNum, SeekOrigin.Begin);
            FileInfo afi = new FileInfo(mArchiveName[0]);
            CopyStreamToStream(br, bw, afi.Length - FileHeaderSize * mFilesNum, DefaultBufferSize);
            br.Close();
            //// STARI SADRZAJ

            CopyFileToStream(bw, fileName, DefaultBufferSize);

            bw.Close();

            mFilesNum++;
            File.Delete(mArchiveName[0]);
            File.Move(mArchiveName[0] + "_", mArchiveName[0]);
        }

        public void AppendFileAs(string fileName, string newName)
        {
            if (!File.Exists(fileName)) return;

            ShiftOffsets(FileHeaderSize);

            FileHeader[] tFiles = new FileHeader[mFilesNum + 1];
            tFiles[mFilesNum] = GetHeaderFor(fileName);
            tFiles[mFilesNum].Name = newName;

            for (int i = 0; i < mFilesNum; ++i)
                tFiles[i] = mFiles[i];

            mFiles = tFiles;

            if (mFilesNum > 0)
                mFiles[mFilesNum].Offset = mFiles[mFilesNum - 1].Offset + mFiles[mFilesNum - 1].Length;
            else
                mFiles[mFilesNum].Offset = BaseHeader;


            BinaryWriter bw = new BinaryWriter(File.Open(mArchiveName[0] + "_", FileMode.Create));
            mFilesNum++;
            WriteHeader(bw);
            mFilesNum--;

            BinaryReader br = new BinaryReader(File.Open(mArchiveName[0], FileMode.Open));
            //for (int i = 0; i < BaseHeader + FileHeaderSize * mFilesNum; ++i)
            //    br.ReadByte();
            br.BaseStream.Seek(BaseHeader + FileHeaderSize * mFilesNum, SeekOrigin.Begin);
            FileInfo afi = new FileInfo(mArchiveName[0]);
            CopyStreamToStream(br, bw, afi.Length - FileHeaderSize * mFilesNum, DefaultBufferSize);
            br.Close();
            //// STARI SADRZAJ

            CopyFileToStream(bw, fileName, DefaultBufferSize);

            bw.Close();

            mFilesNum++;
            File.Delete(mArchiveName[0]);
            File.Move(mArchiveName[0] + "_", mArchiveName[0]);
        }

        private FileHeader GetHeaderFor(string fileName)
        {
            FileHeader h = new FileHeader();
            FileInfo fi = new FileInfo(fileName);
            h.Path = "";
            h.Name = fi.Name;
            h.Length = fi.Length;
            h.StartingFile = 0;
            h.Flags = 0;
            h.Offset = 0;
            return h;
        }
    }
}
